package redisx

import (
	"context"
	"errors"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"

	"gitee.com/hasika/gotool/logx/stdlog"
)

var Log = stdlog.NewDefaultLog()

var (
	ErrLockTimeout = errors.New("get the lock time out")
	ErrLockFailed  = errors.New("get the lock failed")
)

type Cfg struct {
	MaxTryTime      time.Duration //重试时间
	RetryInterval   time.Duration //最小重试间隔
	ExtendValidTime time.Duration //锁失效时间
	ExtendInterval  time.Duration //刷新锁间隔
}

type RedisLock struct {
	rdb redis.UniversalClient
	*Cfg
	key        string
	value      *string
	ctx        context.Context
	cancel     context.CancelFunc
	lockValue  string
	extendStop sync.WaitGroup
}

func NewRedisLock(rdb redis.UniversalClient, key string, cfg *Cfg) *RedisLock {
	if cfg.MaxTryTime <= 0 {
		cfg.MaxTryTime = time.Second * 1
	}

	if cfg.RetryInterval == 0 {
		cfg.RetryInterval = 5 * time.Millisecond
	}

	if cfg.ExtendValidTime == 0 {
		cfg.ExtendValidTime = time.Second * 8
	}

	if cfg.ExtendValidTime < time.Millisecond*10 {
		cfg.ExtendValidTime = time.Millisecond * 10
	}

	interval := cfg.ExtendValidTime / 3
	if cfg.ExtendInterval > interval || cfg.ExtendInterval <= 0 {
		cfg.ExtendInterval = interval
	}
	ctx, cancel := context.WithCancel(context.Background())
	return &RedisLock{
		rdb:    rdb,
		Cfg:    cfg,
		key:    key,
		ctx:    ctx,
		cancel: cancel,
	}
}

func (r *RedisLock) UnLock() {
	if len(r.lockValue) == 0 {
		Log.Warnf("redis lock value is empty key %+v", r.key)
		return
	}
	r.cancel()
	r.extendStop.Wait()
	_, err := r.rdb.Eval(context.Background(), `
    if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
    else
        return 0
    end`, []string{r.key}, r.lockValue).Result()
	if err != nil {
		Log.Warnf("redis lock unlock key %+v error %+v", r.key, err)
	}
}

func (r *RedisLock) TryLock() error {
	success, err := r.tryLock()
	if success {
		return nil
	}
	if err != nil {
		return err
	}
	return ErrLockFailed
}

func (r *RedisLock) Lock() error {
	ticker := time.NewTicker(r.RetryInterval)
	defer ticker.Stop()
	lockCtx, lockCancel := context.WithTimeout(r.ctx, r.MaxTryTime)
	defer lockCancel()
	for {
		select {
		case <-lockCtx.Done():
			return ErrLockTimeout
		case <-ticker.C:
			success, err := r.tryLock()
			if success {
				return nil
			}
			if err != nil {
				return err
			}
		}
	}
}

func (r *RedisLock) tryLock() (bool, error) {
	if r.lockValue != "" {
		return false, errors.New("already locked")
	}
	lockValue := uuid.New().String()
	ok, err := r.rdb.SetNX(context.Background(), r.key, lockValue, r.ExtendValidTime).Result()
	if ok {
		r.lockValue = lockValue
		go func() {
			r.extendBackground()
		}()
		return true, nil
	}
	return false, err
}

func (r *RedisLock) extendBackground() {
	r.extendStop.Add(1)
	defer r.extendStop.Done()
	defer r.cancel()
	extendTicker := time.NewTicker(r.ExtendInterval)
	defer extendTicker.Stop()
	for {
		select {
		case <-extendTicker.C:
			err := r.extendOnce()
			if err != nil {
				Log.Errorf("failed to extend ,key %+v err: %+v", r.key, err)
				return
			}
		case <-r.ctx.Done():
			return
		}
	}
}

func (r *RedisLock) extendOnce() error {
	_, err := r.rdb.Eval(context.Background(), `
local currentValue=redis.call("GET", KEYS[1])
if  currentValue== ARGV[1] then
	return redis.call("pexpire", KEYS[1],ARGV[2])
else
	if currentValue==false then
		return redis.error_reply("key not found")
	end
	local errMsg="current value not match,saved " .. currentValue .. " input " .. ARGV[1]
	return redis.error_reply(errMsg)
end
`, []string{r.key}, r.lockValue, r.ExtendValidTime.Milliseconds()).Result()
	return err
}
